package bf.runner;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bf.util.BFProgram;

import com.sun.org.apache.bcel.internal.Constants;

public class BF2BytecodeRunner3 extends ClassLoader implements Constants {

	static int methodCount = 0;
	private static final String BASE = "bf/util/ObjectBF";
	private static final String CLASS_NAME = "bf";

	public static void main (final String args[]) throws Exception {
		System.out.println("compile and run: " + args[0]);
		long time1 = System.currentTimeMillis();
		run(new BufferedReader(new FileReader(args[0])));
		long time2 = System.currentTimeMillis();
		System.out.println();
		System.out.println(time2 - time1 + " ms");
	}

	private static void run(Reader reader) throws IOException, InstantiationException, IllegalAccessException {
		byte[] bytecode = compileInfo(reader);
		Class<BFProgram> bfc = load(bytecode);
		BFProgram bf = bfc.newInstance();
		bf.run();
	}

	private static byte[] compileInfo(Reader reader) throws IOException {
		byte[] code = compile(reader);
		System.out.println("Compiled code size: " + code.length + " bytes");
		return code;
	}
	
	private static byte[] compile(Reader reader) throws IOException {
		ClassWriter cw = new ClassWriter(0);
		cw.visit(Opcodes.V1_6, ACC_PUBLIC, CLASS_NAME, null, BASE, null);

		{
			MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
			mv.visitCode();
			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitLineNumber(3, l0);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitMethodInsn(INVOKESPECIAL, BASE, "<init>", "()V");
			mv.visitInsn(RETURN);
			Label l1 = new Label();
			mv.visitLabel(l1);
			mv.visitLocalVariable("this", "Lbf;", null, l0, l1, 0);
			mv.visitMaxs(1, 1);
			mv.visitEnd();
		}
		
		{
			MethodVisitor mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
			mv.visitCode();
			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitLineNumber(12, l0);
			mv.visitTypeInsn(NEW, CLASS_NAME);
			mv.visitInsn(DUP);
			mv.visitMethodInsn(INVOKESPECIAL, CLASS_NAME, "<init>", "()V");
			mv.visitVarInsn(ASTORE, 1);
			Label l1 = new Label();
			mv.visitLabel(l1);
			mv.visitLineNumber(13, l1);
			mv.visitVarInsn(ALOAD, 1);
			mv.visitMethodInsn(INVOKEVIRTUAL, CLASS_NAME, "run", "()V");
			Label l2 = new Label();
			mv.visitLabel(l2);
			mv.visitLineNumber(14, l2);
			mv.visitInsn(RETURN);
			Label l3 = new Label();
			mv.visitLabel(l3);
			mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l3, 0);
			mv.visitLocalVariable(CLASS_NAME, "L" + CLASS_NAME + ";", null, l1, l3, 1);
			mv.visitMaxs(2, 2);
			mv.visitEnd();
			}
		
		genMethod(reader, cw);
		
		cw.visitEnd();
		byte[] code = cw.toByteArray();
		return code;
	}

	private static void genMethod(Reader reader, ClassWriter cw)
			throws IOException {
		{
//			System.out.println("genMehhod: " + getName());
//			Stack<Label> labelStack = new Stack<Label>();
			MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, getName(), "()V", null, null);
			mv.visitCode();
			Label l0 = new Label();
			mv.visitLabel(l0);
			
			int read;
			ParsePE:
			while((read = reader.read()) != -1) {
				char cmd = (char)read;
				switch(cmd) {
					case '>':
						genLoadValue(mv, lookAhead(reader, cmd));
						mv.visitMethodInsn(INVOKEVIRTUAL, BASE, "PADD", "(I)V");
						break;
					case '<':
						genLoadValue(mv, -lookAhead(reader, cmd));
						mv.visitMethodInsn(INVOKEVIRTUAL, BASE, "PADD", "(I)V");
						break;
					case '+':
						genLoadValue(mv, lookAhead(reader, cmd));
						mv.visitMethodInsn(INVOKEVIRTUAL, BASE, "CADD", "(I)V");
						break;
					case '-':
						genLoadValue(mv, -lookAhead(reader, cmd));
						mv.visitMethodInsn(INVOKEVIRTUAL, BASE, "CADD", "(I)V");
						break;
					case '.':
						mv.visitVarInsn(ALOAD, 0);
						mv.visitMethodInsn(INVOKEVIRTUAL, BASE, "OC", "()V");
						break;
					case '[':
						if(isClearCell(reader)){
							mv.visitVarInsn(ALOAD, 0);
							mv.visitMethodInsn(INVOKEVIRTUAL, BASE, "CLR", "()V");
						} else {
							Label l11 = new Label();
							mv.visitJumpInsn(GOTO, l11);
							Label l12 = new Label();
							mv.visitLabel(l12);
							mv.visitLineNumber(46, l12);
							mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
							mv.visitVarInsn(ALOAD, 0);
							
							methodCount++;
							mv.visitMethodInsn(INVOKESPECIAL, CLASS_NAME, getName(), "()V");
							genMethod(reader, cw);
							
							mv.visitLabel(l11);
							mv.visitLineNumber(45, l11);
							mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
							mv.visitVarInsn(ALOAD, 0);
							mv.visitFieldInsn(GETFIELD, BASE, "cell", "[B");
							mv.visitVarInsn(ALOAD, 0);
							mv.visitFieldInsn(GETFIELD, BASE, "pointer", "I");
							mv.visitInsn(BALOAD);
							mv.visitJumpInsn(IFNE, l12);
						}
						break;
					case ']':
						break ParsePE;
					default:
//						throw new RuntimeException("Unknown command!");
//						System.out.println("Ignore: " + cmd);
						continue;
				}
			}
			mv.visitInsn(RETURN);
			Label l112 = new Label();
			mv.visitLabel(l112);
			mv.visitLocalVariable("this", "Lbf;", null, l0, l112, 0);
			mv.visitMaxs(2, 1);
			mv.visitEnd();
//			System.out.println("size=" + mv.);
		}
	}

	private static void genLoadValue(MethodVisitor mv, int count) {
		mv.visitVarInsn(ALOAD, 0);
		switch(count) {
		case 0:
			mv.visitInsn(ICONST_0);
			break;
		case 1:
			mv.visitInsn(ICONST_1);
			break;
		case 2:
			mv.visitInsn(ICONST_2);
			break;
		case 3:
			mv.visitInsn(ICONST_3);
			break;
		case 4:
			mv.visitInsn(ICONST_4);
			break;
		case 5:
			mv.visitInsn(ICONST_5);
			break;
		default:
			mv.visitIntInsn(BIPUSH, count);
			break;
		}
	}

	private static boolean isClearCell(Reader reader) throws IOException {
		reader.mark(2);
		if(reader.read() == '-' && reader.read() == ']') {
			return true;
		} else {
			reader.reset();
			return false;
		}
	}
	
	private static int lookAhead(Reader reader, char cmd) throws IOException {
		int count = 1;
		while(true) {
			reader.mark(1);
			if(reader.read() != cmd) break;
			count++;
		}
		reader.reset();
		return count;
	}
	
	private static String getName() {
		String name = "run";
		if(methodCount != 0) {
			name = name.concat("" + methodCount);
		}
		return name;
	}
	
	private static Class<BFProgram> load(byte[] code) {
		BF2BytecodeRunner3 loader = new BF2BytecodeRunner3();
		Class<BFProgram> exampleClass = (Class<BFProgram>) loader.defineClass(CLASS_NAME, code, 0, code.length);
		return exampleClass;
	}

}
